Compare & Ordering(Sort)

#Kotlin #Comparator #Comparable #SortedWith

1. Compare

"1이랑 100 중에 어떤게 더 큰가?"
"apple이랑 zebra 중에 어떤게 사전에서 더 뒤에 오는가?"
이런걸 비교라고 한다.

1-1. Comparable

Comparable Interface 소스코드
Comparable api
위의 두 예제는 비교의 대상이 되는 값의 타입이 각각 Int, String이다.
이렇게 코틀린의 기본 타입들은 각 값들을 비교할 수 있다. oneVal.compareTo(twoVal)과 같이 compareTo() 메서드를 사용해서.

Int, String의 구현에 가보면

public class Int private constructor() : Number(), Comparable<Int> {}

public class String : Comparable<String>, CharSequence {}

이렇게 Comparable<T>를 상속받고 있는것을 확인할 수 있다.

우리가 만든(레퍼런스 타입) 객체들도 비교가 가능하게 만들어 줄 수 있다. Comparable을 사용해서 객체 자체가 비교가 가능한 객체로 인식되도록 만드는 것이다.

public class User(val id: Int, val name: String, val level: Int) : Comparable<User> {
	override fun compareTo(other: User): Int {
		return when {
			this.id > other.id -> 1
			this.id < other.id -> -1
			else -> 0
		}
	}
}

fun main() {
	val user1 = User(1, "one", 100)
	val user2 = User(2, "two", 200)

	println("user1 > user2? => user1.compareTo(user2)")
}

=> 객체 자체를 비교 가능한 객체로 만들어 주기 위해 사용하는 인터페이스

객체마다 compare의 기준은 하나씩이다.

그런데 여기서 어떤 다른 기준을 가지고 정렬하고 싶다. name이나 level를 이용해서. 라고 했을 때에는 어떻게 해야할까?

1-2. Comparator

그럴 때 Comparator을 사용하면 된다.

object compareNameTo : Comparator<User> {
	override fun compare(user1: User, user2: User): Int {
		return when {
			user1.id > user2.id -> 1
			user1.id < user2.id -> -1
			else -> 0
		}
	}
}

object compareLevelTo : Comparator<User> {
	override fun compare(user1: User, user2: User): Int {
		return when {
			user1.level > user2.level -> 1
			user1.level < user2.level -> -1
			else -> 0
		}
	}
}

이것들을 가지고 정렬 메서드에 적용시켜주면 된다.


2. Ordering

Custom Ordering 공식문서
일반적으로 숫자, 알파벳등의 기준으로 정렬을 하고자 하면 sorted()를 사용하면 된다.

User도 sorted()로 정렬할 수 있을까?
=> 당연하다. sorted는 comparable인터페이스의 확장함수니까!
이것이 우리가 만든 객체를 Comparable로 만드는 핵심 이유이기도 하다.

그럼 name이나 level 프로퍼티로 정렬하고 싶을때는?
=> sortedBy() or sortedWith()Comparable or Comparator를 상속받은 객체를 이용해 사용하면 된다.

fun main() {
	val user1 = User(1, "one", 100)
	val user2 = User(2, "two", 200)
	val user3 = User(3, "three", 300)
	val userArr = arrayOf(user3, user1, user2)

	val sortedByName = userArr.sortedWith(compareNameTo)
	println("sorted by user name: ${sortedByName.map{user -> user.id}}")
}

위의 예제에서 왜 sortedBy가 아니라 sortedWith를 썼을까?

2-1. sortedBy()

sortedBy() api

inline fun <T, R : Comparable<R>> Array<out T>.sortedBy(
    crossinline selector: (T) -> R?
): List<T>

sortedBy()의 파라미터는 함수타입이다. 즉 comparator 타입이 아니라는 말이다.
간단하게 재활용을 생각하지 않고 기준만 잡고, 정렬만 해내고 싶을 때 사용하는게 sortedBy()이다.

fun main() {
	val sortedByName = userArr.sortedBy{
		it.name
	}

	println("sorted by user name: ${sortedByName.map{user -> user.id}}")
}

2-2. sortedWith()

sortedWith() api

fun <T> Array<out T>.sortedWith(
    comparator: Comparator<in T>
): List<T>

반면 sortedWith()의 파라미터는 Comparator 타입이다.

그럼 왜 굳이굳이 Comparator을 만들어서 사용하느냐?
가장 기본적인 이유로는 재활용성을 꼽을 수 있다.
그리고 대망의 이유는! '여러 기준'을 적용할 수 있다.
Comparator은 확장함수 then()을 가지기 때문에!
예를 들어, level 순으로 정렬한 다음, 같은 level을 가진 유저들을 다시 이름순으로 정렬하고 싶다. 와 같을 때 사용한다.

fun main() {
	val chainComparator = compareLevelTo.then(compareNameTo)
	val sortedByLevelAndName = userArr.sortedWith(chainComparator)

	println("sorted by user level and then name: ${sortedByLevelAndName.map{user -> user.id}}")
}

여기서 하나더.
굳이굳이 재사용을 할 필요가 없는데, 기준을 여러개로 만들고 싶다?
compareBy()함수를 이용하자. 이것은 Comparable을 리턴해주기만 하면 된다. 여러개를 리턴해도 된다.
compareBy api

fun main() {
	val sortedByLevelAndName = userArr.sortedWith(compareBy(
		{it.level},
		{it.name}
	))

	println("sorted by user level and then name: ${sortedByLevelAndName.map{user -> user.id}}")
}

이 compareBy는 함수들, 함수 하나, comparator+함수를 파라미터로 받을 수 있다.
그리고 compareBy()는 결국 comparator를 반환한다.